namespace egret.socketio.engine {

    export class Socket extends Emitter {
        private transport: Transport;

        // private secure: boolean;

        // private hostname: string;
        // private port: number;
        // private query: Map<string, Primitive>;
        // private upgrade: boolean;
        // private path: string;
        // private timestampParam: string;
        // private timestampRequests: boolean;
        // private transports: Array<string>;
        // private policyPort: number;
        // private rememberUpgrade: boolean;
        // private transportOptions: Map<string, TransportOptions>;
        static priorWebsocketSuccess = false;

        private opts: SocketUri;

        private readyState: _emReadyState;
        public id: string = null;
        private upgrades = null;
        private upgrading = false;
        private pingInterval = null;
        private pingTimeout = null;

        private pingIntervalTimer = null;
        private pingTimeoutTimer = null;

        private writeBuffer: Packet[] = [];
        private prevBufferLen: number = 0;

        constructor(uri?: string | SocketUri) {
            super();

            if (typeof uri === 'string') {
                uri = new SocketUri(uri);
            }

            this.readyState = _emReadyState.CLOSED;
            this.opts = uri.clone();
            if (!uri.port) {
                this.opts.port = uri.secure ? 443 : 80;
            }
            this.opts.transports = uri.transports || [Polling.NAME, WebSocket.NAME];

            // this.secure = opts.secure;
            // if (!opts.port) {
            //     opts.port = this.secure ? 443 : 80;
            // }

            // this.hostname = opts.hostname || 'localhost';
            // this.port = opts.port;
            // this.query = parseQuery(opts.query);
            // this.upgrade = opts.upgrade;
            // this.path = (opts.path || '/engine.io').replace('/$', '') + '/';
            // this.timestampParam = opts.timestampParam || 't';
            // this.timestampRequests = opts.timestampRequests;
            // this.transports = opts.transports || [Polling.NAME, WebSocket.NAME];
            // this.transportOptions = opts.transportOptions || new Map();
            // this.policyPort = opts.policyPort;
            // this.rememberUpgrade = opts.rememberUpgrade;
            this.open();
        }

        public open(): Socket {

            new Promise((reslove, reject) => {
                let transportName: string;
                if (this.opts.rememberUpgrade && Socket.priorWebsocketSuccess && this.opts.transports.indexOf(WebSocket.NAME) !== -1) {
                    transportName = WebSocket.NAME;
                } else if (0 === this.opts.transports.length) {
                    this.emit(_emEvent.EVENT_ERROR, "No transports available");
                    return;
                } else {
                    transportName = this.opts.transports[0];
                }
                this.readyState = _emReadyState.OPENING;
                let transport = this.createTransport(transportName);
                this.setTransport(transport);
                transport.open();
            });

            return this;
        }

        public flush(): void {

            if (this.readyState !== _emReadyState.CLOSED && this.transport.writable && !this.upgrading && this.writeBuffer.length) {
                this.transport.send(this.writeBuffer);
                this.prevBufferLen = this.writeBuffer.length;
                this.emit(_emEvent.EVENT_FLUSH);
            }

        }

        public probe(name: string): void {

            let transport = this.createTransport(name, { probe: 1 });
            let failed = false;

            Socket.priorWebsocketSuccess = false;

            let onTransportOpen = () => {
                // if (self.onlyBinaryUpgrades) {
                //     var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary;
                //     failed = failed || upgradeLosesBinary;
                //   }

                if (failed) return;

                transport.send([{ type: _emPacketType[_emPacketType.ping], data: 'probe' }]);
                transport.once(_emEvent.EVENT_PACKET, (msg: Packet) => {
                    if (failed) return;
                    if (_emPacketType[msg.type] === _emPacketType.pong && msg.data === 'probe') {
                        this.upgrading = true;
                        this.emit(_emEvent.EVENT_UPGRADING, transport);
                        if (!transport) return;

                        Socket.priorWebsocketSuccess = WebSocket.NAME === transport.name;
                        this.transport.pause(() => {
                            if (failed) return;
                            if (this.readyState === _emReadyState.CLOSED) return;

                            cleanup();
                            this.setTransport(transport);
                            transport.send([{ type: _emPacketType[_emPacketType.upgrade] }]);
                            this.emit(_emEvent.EVENT_UPGRADE, transport);
                            transport = null;
                            this.upgrading = false;
                            this.flush();
                        });

                    } else {
                        let err = new Error('probe error');
                        err.transport = transport.name;
                        this.emit(_emEvent.EVENT_UPGRADE_ERROR, err);
                    }
                });
            };

            let freezeTransport = () => {
                if (failed) return;

                failed = true;

                cleanup();
                transport && transport.close();
                transport = null;
            };

            let onerror = (err?: string) => {
                let error = new Error('probe error: ' + err);
                error.transport = transport && transport.name;

                freezeTransport();
                this.emit(_emEvent.EVENT_UPGRADE_ERROR, error);
            };

            let onTransportClose = () => {
                onerror('transport closed');
            };

            let onclose = () => {
                onerror('socket closed');
            }

            let onupgrade = (to) => {
                if (transport && to.name != transport.name) {
                    freezeTransport();
                }
            }

            let cleanup = () => {
                transport && transport.off(_emEvent.EVENT_OPEN, onTransportOpen);
                transport && transport.off(_emEvent.EVENT_ERROR, onerror);
                transport && transport.off(_emEvent.EVENT_CLOSE, onclose);
                this.off(_emEvent.EVENT_CLOSE, onclose);
                this.off(_emEvent.EVENT_UPGRADING, onupgrade);
            }

            transport.once(_emEvent.EVENT_OPEN, onTransportOpen);
            transport.once(_emEvent.EVENT_ERROR, onerror);
            transport.once(_emEvent.EVENT_CLOSE, onclose);

            this.once(_emEvent.EVENT_CLOSE, onclose);
            this.once(_emEvent.EVENT_UPGRADING, onupgrade);

            transport.open();
        }

        private ping(): void {
            this.sendPacket(_emPacketType[_emPacketType.ping], () => {
                this.emit(_emEvent.EVENT_PING);
            });
        }

        private createTransport(name: string, probe?: any): Transport {

            let options = this.opts.transportOptions[name];

            let opts = new TransportUri(this.opts.toString());
            opts.addQueryParam('EIO', 3);
            opts.addQueryParam('transport', name);
            if (this.id) {
                opts.addQueryParam('sid', this.id);
            }


            opts.hostname = (options && options.hostname) || this.opts.hostname;
            opts.port = (options && options.port) || this.opts.port;
            opts.secure = (options && options.secure) || this.opts.secure;
            opts.path = (options && options.path) || this.opts.path;
            opts.timestampRequests = (options && options.timestampRequests) || this.opts.timestampRequests;
            opts.timestampParam = (options && options.timestampParam) || this.opts.timestampParam;
            opts.policyPort = (options && options.policyPort) || this.opts.policyPort;

            let transport: Transport;
            if (WebSocket.NAME === name) {
                transport = new WebSocket(opts);
            } else if (Polling.NAME === name) {
                transport = new PollingXHR(opts);
            } else {
                return null;
            }

            this.emit(_emEvent.EVENT_TRANSPORT, transport);
            return transport;
        }

        private setTransport(transport: Transport): void {

            if (this.transport) {
                this.transport.off();
            }

            this.transport = transport;

            transport.on(_emEvent.EVENT_DRAIN, this.onDrain, this)
                .on(_emEvent.EVENT_PACKET, this.onPacket, this)
                .on(_emEvent.EVENT_ERROR, this.onError, this)
                .on(_emEvent.EVENT_CLOSE, this.onClose, this);
        }


        private onDrain(): void {
            this.writeBuffer.splice(0, this.prevBufferLen);

            // setting prevBufferLen = 0 is very important
            // for example, when upgrading, upgrade packet is sent over,
            // and a nonzero prevBufferLen could cause problems on `drain`
            this.prevBufferLen = 0;

            if (0 === this.writeBuffer.length) {
                this.emit('drain');
            } else {
                this.flush();
            }
        }

        private onPacket(packet: Packet): void {

            if (this.readyState === _emReadyState.OPENING ||
                this.readyState === _emReadyState.OPEN ||
                this.readyState === _emReadyState.CLOSING) {

                this.emit(_emEvent.EVENT_PACKET, packet);
                this.emit(_emEvent.EVENT_HEARTBEAT);

                switch (_emPacketType[packet.type]) {
                    case _emPacketType.open: {
                        this.onHandshake(JSON.parse(packet.data));
                        break;
                    }
                    case _emPacketType.pong: {
                        this.setPing();
                        this.emit(_emEvent.EVENT_PONG);
                        break;
                    }
                    case _emPacketType.error: {
                        let err = new Error('server error');
                        err.code = packet.data;
                        this.onError(err);
                        break;
                    }
                    case _emPacketType.message: {
                        this.emit(_emEvent.EVENT_DATA, packet.data);
                        this.emit(_emEvent.EVENT_MESSAGE, packet.data);
                        break;
                    }
                }
            } else {
                console.log('packet received with socket readyState ' + this.readyState);
            }
        }

        private onError(err: Error): void {
            console.debug('socket error ' + err);
            Socket.priorWebsocketSuccess = false;
            this.emit('error', err);
            this.onClose('transport error', err);
        }

        private onOpen(): void {
            console.debug('socket open');
            this.readyState = _emReadyState.OPEN;
            Socket.priorWebsocketSuccess = this.transport.name === WebSocket.NAME;
            this.emit(_emEvent.EVENT_OPEN);
            this.flush();

            if (this.readyState === _emReadyState.OPEN && this.upgrades && this.transport.pause) {
                console.debug('starting upgrade probes');
                for (var i = 0, l = this.upgrades.length; i < l; i++) {
                    this.probe(this.upgrades[i]);
                }
            }
        }

        private onClose(reason?: string, desc?: any): void {
            if (this.readyState === _emReadyState.OPENING ||
                this.readyState === _emReadyState.OPEN ||
                this.readyState === _emReadyState.CLOSING) {
                console.debug('socket close with reason: ' + reason);

                clearTimeout(this.pingIntervalTimer);
                clearTimeout(this.pingTimeoutTimer);

                this.transport.off(_emEvent.EVENT_CLOSE);
                this.transport.close();
                this.transport.off();

                this.readyState = _emReadyState.CLOSED;
                this.id = null;

                this.emit(_emEvent.EVENT_CLOSE, reason, desc);
            }
        }

        close(): Socket {

            let close = () => {
                this.onClose('forced close');
                console.debug('socket closing - telling transport to close');
                this.transport.close();
            };

            let cleanupAndClose = () => {
                this.off(_emEvent.EVENT_UPGRADE, cleanupAndClose);
                this.off(_emEvent.EVENT_UPGRADE_ERROR, cleanupAndClose);
            };

            let waitForUpgrade = () => {
                this.once(_emEvent.EVENT_UPGRADE, cleanupAndClose);
                this.once(_emEvent.EVENT_UPGRADE_ERROR, cleanupAndClose);
            };


            if (this.readyState === _emReadyState.OPENING || this.readyState === _emReadyState.OPEN) {
                this.readyState = _emReadyState.CLOSING;

                if (this.writeBuffer.length) {
                    this.once(_emEvent.EVENT_DRAIN, () => {
                        if (this.upgrading) {
                            waitForUpgrade();
                        } else {
                            close();
                        }
                    });
                } else if (this.upgrading) {
                    waitForUpgrade();
                } else {
                    close();
                }
            }

            return this;
        }

        private onHandshake(data: any): void {
            this.emit(_emEvent.EVENT_HANDSHAKE, data);
            this.id = data.sid;
            this.transport.opts.replaceQueryParam('sid', data.sid);
            this.upgrades = this.filterUpgrades(data.upgrades);
            this.pingInterval = data.pingInterval;
            this.pingTimeout = data.pingTimeout;
            this.onOpen();

            if (this.readyState === _emReadyState.CLOSED) return;
            this.setPing();

            this.off(_emEvent.EVENT_HEARTBEAT, this.onHeartbeat, this);
            this.on(_emEvent.EVENT_HEARTBEAT, this.onHeartbeat, this);
        }

        private setPing(): void {

            clearTimeout(this.pingIntervalTimer);
            this.pingIntervalTimer = setTimeout(() => {
                console.debug('writing ping packet - expecting pong within ' + this.pingTimeout + 'ms');
                this.ping();
                this.onHeartbeat(this.pingTimeout);
            }, this, this.pingInterval);

        }

        private onHeartbeat(timeout: number): void {
            clearTimeout(this.pingTimeoutTimer);

            this.pingTimeoutTimer = setTimeout(() => {
                if (this.readyState === _emReadyState.CLOSED) return;
                this.onClose('ping timeout');
            }, this, timeout || (this.pingInterval + this.pingTimeout));
        }

        public write(msg: string, options?: Object, fn?: Function): Socket {
            return this.send(msg, options, fn);
        }

        public send(msg: string, options?: Object, fn?: Function): Socket {
            this.sendPacket('message', msg, options, fn);
            return this;
        }

        private sendPacket(type: string, data: string);
        private sendPacket(type: string, fn: Function);
        private sendPacket(type: string, data: string, options: Object);
        private sendPacket(type: string, data: string, fn: Function);
        private sendPacket(type: string, data: string, options: Object, fn: Function);

        private sendPacket(type: string, data?: any, options?: any, fn?: any) {
            if (typeof data === 'function') {
                fn = data;
                data = undefined;
            }

            if (typeof options === 'function') {
                fn = options;
                options = null;
            }

            if (this.readyState === _emReadyState.CLOSING || this.readyState === _emReadyState.CLOSED) {
                return;
            }

            options = options || {};
            let packet: Packet = {
                type: type,
                data: data,
                options: options
            }
            this.emit(_emEvent.EVENT_PACKET_CREATE, packet);
            this.writeBuffer.push(packet);
            if (fn) this.once(_emEvent.EVENT_FLUSH, fn);
            this.flush();
        }

        private filterUpgrades(upgrades: string[]): string[] {
            let filteredUpgrades: string[] = [];
            for (let i = 0; i < upgrades.length; i++) {
                if (this.opts.transports.indexOf(upgrades[i]))
                    filteredUpgrades.push(upgrades[i]);
            }
            return filteredUpgrades;
        }
    }
}